﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Xml.Linq;

using SysEnv = System.Environment;

using King.Extensions;
using King.Automation;
using King.Text;
using King.Reflection;

namespace King.TeamFoundation {

    public class Program {

        public static int Main(string[] args) {
            return ConsoleApplication.EntryPoint(args);
        }
    }

    public abstract class TfsCommand {

        private string m_path;

        protected TfsLoader Loader { get; set; }
        protected TfsCollection Collection { get; set; }
        protected TfsFolder LabFolder { get; set; }
        protected TfsWorkspace Workspace { get; set; }
        protected TfsFolder CurrentFolder { get; set; }

        internal TfsCommand()
            : this(SysEnv.CurrentDirectory.NormalizeDirectory()) {
        }
        internal TfsCommand(string path) {
            var vsCommonTools = SysEnv.GetEnvironmentVariable("VS100COMNTOOLS");
            if (vsCommonTools != null) {
                var vsCommonToolsUrl = new Uri(vsCommonTools);
                var devEnvDir = new Uri(vsCommonToolsUrl, @"..\IDE");
                SysEnv.SetEnvironmentVariable("PATH", SysEnv.GetEnvironmentVariable("PATH") + ";" + devEnvDir.LocalPath);
                SysEnv.SetEnvironmentVariable("PATH", SysEnv.GetEnvironmentVariable("PATH") + ";" + @"C:\tfs3\private\user\cking\lab2\dev\bin\tfpt\");
            }

            m_path = path;
            Loader = new TfsLoader(Path.GetDirectoryName(m_path));
            Collection = Loader.Collection;
            Workspace = Collection.Workspace;
            LabFolder = Collection.LabFolder;
            CurrentFolder = Workspace.CurrentFolder.GetFolder(m_path);
        }
    }

    /// <summary>
    /// Tranform .sln and .vdproj files to and from an xml representation. 
    /// </summary>
    [Command]
    public class Xml {

        // xml C:\tfs\private\user\cking\lab2\dev\src\King.LogProcessor\Installer\Installer.vdproj
        public void Execute() {

            var extension = Path.GetExtension(Source);
            if (extension == ".xml") {
                var target = Source.Remove(".xml");
                extension = Path.GetExtension(target);
                var info = TfsLoader.GetInfo(extension);

                if (Target == null)
                    Target = target;

                var sln = info.FromXml(XElement.Load(Source));
                File.WriteAllText(Target, sln, Encoding.UTF8);

            } else {
                var info = TfsLoader.GetInfo(extension);

                if (Target == null)
                    Target = Environment.CurrentDirectory + "\\" + Path.GetFileName(Source) + ".xml";

                info.ToXml(Source).Save(Target);
            }
        }

        [CommandTarget(1, IsRelativeToCurrentDirectory = true, IsOptional = true)]
        public string Source { get; set; }

        [CommandTarget(2, IsRelativeToCurrentDirectory = true, IsOptional = true)]
        public string Target { get; set; }
    }

    /// <summary>
    /// Rename the source to the target and fix project and file references. In the common case 
    /// where source is a directory and contains a single project whose name is (or is composed of) the 
    /// target name then that project will be renamed (or recomposed) using the target name in 
    /// addition to renaming the source directory. (e.g. Foo\Acme.Foo.csproj -> Bar\Acme.Bar.csproj)
    /// </summary>
    [Command]
    public class Rename : TfsCommand {

        public IEnumerable<object> Execute() {

            var sourceFolder = Collection.GetFolder(Source);
            var sourceUrl = Workspace.GetUrl(Source);

            var status = Collection.Status();
            var openFilesInSource = status.Where(o => o.Path.Url.IsSubUrlOrEqual(sourceUrl));
            if (openFilesInSource.Any() && !Force) {
                yield return new TextHeader("The following files are being manipulated in this or other workspaces: ") {
                    from o in openFilesInSource
                    let path = o.Path
                    from s in path.StatusElsewhere()
                    orderby path.Path
                    select s.ToString()
                };

                yield break;
            }

            var source = Collection.GetPath(Source);
            if (source.IsFolder)
                Target = Target.NormalizeDirectory();

            // cache all projects and solutions
            LabFolder.LoadProjectGraph();

            // discover any projects in the source directory before they are deleted
            var projects = source.Get<TfsProject>();

            // execute rename
            source.Rename(Target);

            if (source.IsFolder && projects.OnlyOne()) {
                var project = projects.Single();

                var sourceFolderName = sourceFolder.Name.ToUpperFirst();
                var targetFolderName = Collection.GetFolder(Target).Name.ToUpperFirst();
                var projectName = Path.GetFileNameWithoutExtension(project.Name);
                if (projectName.Contains(sourceFolderName)) {

                    // rename the project by replacing the source folder name with the target folder name
                    var branchedProject = Collection.GetItem(new Uri(new Uri(Target), new Uri(Source).MakeRelativePath(project.Path)).LocalPath);
                    var targetName = branchedProject.Name.Replace(sourceFolderName, targetFolderName);
                    var renamedProject = (TfsItem)branchedProject.Rename(targetName);

                    // for C# projects also rename the assembly and root namespace
                    var csProject = renamedProject as TfsCsProject;
                    if (csProject != null) {

                        // rename assembly
                        csProject.AssemblyName = csProject.AssemblyName.Replace(sourceFolderName, targetFolderName);

                        // rename root namespace
                        csProject.RootNamespace = csProject.RootNamespace.Replace(sourceFolderName, targetFolderName);
                    }
                }
            }

            Collection.WriteChanges();
        }

        /// <summary>
        /// Source directory for the branch.
        /// </summary>
        [CommandTarget(1, IsExistingDirectoryOrFile = true, IsRelativeToCurrentDirectory = true)]
        public string Source { get; set; }

        /// <summary>
        /// Target directory for the branch.
        /// </summary>
        [CommandTarget(2, IsRelativeToCurrentDirectory = true)]
        public string Target { get; set; }

        /// <summary>
        /// Force deletion of the source (move must be specified) even if other users 
        /// have checked out files in the source sub-tree.
        /// </summary>
        [CommandSwitch(1)]
        public bool Force { get; set; }

        /// <summary>
        /// If Source is a directory containing a single project in all it's sub-directories 
        /// then that project, it's AssemblyName, and RootNamespace will be renamed in addition to 
        /// branching the source directory unless this flag is set. 
        /// </summary>
        [CommandSwitch(2)]
        public bool NoRename { get; set; }
    }

    /// <summary>
    /// Verify project references are valid and projects guids are unique.
    /// </summary>
    [Command]
    public class Verify : TfsCommand {

        public Verify(string path)
            : base(path) {
        }

        public IEnumerable<object> Execute() {
            var pass = true;

            if (Graph) {
                Console.WriteLine("Verifying Graph");
                CurrentFolder.VerifyProjectGraph();
            }

            if (Guids) {
                Console.WriteLine("Verifying Guids");
                var items = CurrentFolder.Get<TfsProject>();
                var projects = items.GroupBy(o => o.Guid).ToArray();
                var duplicates = projects.Where(o => o.MoreThanOne() && o.Key != null);

                if (duplicates.Any()) {
                    pass = false;
                    yield return new TextHeader(
                        "The following projects have duplicate guids") {
                        from o in duplicates
                        select new TextHeader(o.Key.ToString()) {
                            o.Select(x => x.Path)
                        }
                    };
                }
            }

            if (!pass)
                throw new ArgumentException();
        }

        /// <summary>
        /// Verify references to projects from projects and solutions are valid. Verify existance of 
        /// expected properties of all solutions and projects as well.
        /// </summary>
        [CommandSwitch(1)]
        public bool Graph { get; set; }

        /// <summary>
        /// Enforce that projects have unique GUIDs. The most common cause of duplicate GUIDs is creating a 
        /// new project by copying an existing one without assigning it a new project GUID.
        /// </summary>
        [CommandSwitch(1)]
        public bool Guids { get; set; }
    }

    /// <summary>
    /// Enforce solution and project policy for ToolVersion, SolutionVersion, and build events, etc.
    /// </summary>
    [Command]
    public class Enforce : TfsCommand {

        private bool m_pass = true;

        public Enforce(string path)
            : base(path) {

            var projs = LabFolder.Get<TfsWdProject>();
            foreach (var proj in projs) {
                var xml = proj.Xml.Descendants(TfsCsProject.XNamespace + "TargetFrameworkVersion").SingleOrDefault();
                if (xml == null)
                    continue;
                xml.Value = "v4.0";
            }

            Collection.WriteChanges();
        }

        private void EnforceToolsVersion() {
            Console.WriteLine("Enforce Tools Version");

            var projectsWithToolsVersion =
                from o in TfsLoader.Infos()
                where o.Type.GetProperty("ToolsVersion") != null
                select o.Type;

            var projects = CurrentFolder.Get<TfsProject>(projectsWithToolsVersion);

            var badProjects =
               (from o in projects
                let property = o.GetType().GetProperty("ToolsVersion")
                let toolVersions = (double)property.GetValue(o)
                where toolVersions != ToolsVersion
                orderby o.Path
                select new { ToolVersions = toolVersions, Property = property, Project = o }).ToArray();

            if (badProjects.Any()) {
                m_pass = false;
                Console.WriteLine(new TextHeader(
                    "The following projects have a ToolsVersion other than " + ToolsVersion) {
                    from o in badProjects
                    select new TextJoin(" ") { o.Project.Path, o.ToolVersions.ToString() }
                }.ReadToEnd());
            }

            if (Fix) {
                foreach (var o in badProjects)
                    o.Property.SetValue(o.Project, ToolsVersion);
            }
        }
        private IEnumerable<object> EnforceSolutionVersion() {
            Console.WriteLine("Enforce Solution Version");

            var solutions = CurrentFolder.Get<TfsSolution>();

            var badSolutions =
               (from o in solutions
                where o.Year != SolutionVersion
                orderby o.Path
                select o).ToArray();

            if (badSolutions.Any()) {
                m_pass = false;
                yield return new TextHeader(
                    "The following solutions have a version other than " + SolutionVersion) {
                    from o in badSolutions
                    select new TextJoin(" ") { o.Path, o.Year.ToString() }
                };
            }
        }
        private IEnumerable<object> EnforceBuildEvents() {
            Console.WriteLine("Enforce Build Events");

            var csProjects = CurrentFolder.Get<TfsCsProject>();

            foreach (var csProject in csProjects) {
                var newPreBuildEvent = EnforceRunOnlyInVisualStudio(csProject, csProject.PreBuildEvent);
                if (newPreBuildEvent != null)
                    csProject.PreBuildEvent = newPreBuildEvent;

                var oldPreBuildEvent = EnforceRunOnlyInVisualStudio(csProject, csProject.PostBuildEvent);
                if (oldPreBuildEvent != null)
                    csProject.PreBuildEvent = oldPreBuildEvent;
            }

            if (csProjects.Any(o => o.IsDirty)) {
                yield return new TextHeader(
                    "The following C# projects have (pre|post)BuildSteps steps that fail the regex: " +
                    "^IF \"[$][(]BuildingInsideVisualStudio[)]\" == \"true\" (.*)$") {
                    };

                m_pass = false;
            }
        }
        private string EnforceRunOnlyInVisualStudio(object target, string buildEvent) {
            if (string.IsNullOrEmpty(buildEvent))
                return buildEvent;

            var options = RegexOptions.Singleline | RegexOptions.IgnoreCase;
            var result = Regex.IsMatch(buildEvent, "^IF \"[$][(]BuildingInsideVisualStudio[)]\" == \"true\" (.*)$", options);

            if (!result)
                return "IF \"$(BuildingInsideVisualStudio)\" == \"true\" (" + Environment.NewLine +
                    buildEvent + Environment.NewLine +
                ")";

            return null;
        }
        private void EnforceTargetFrameworkVersion() {
            var projects =
                from o in CurrentFolder.Get<TfsCsProject>()
                where o.TargetFrameworkVersion != TargetFrameworkVersion
                select o;

            foreach (var o in projects)
                o.TargetFrameworkVersion = (double)TargetFrameworkVersion;

            if (projects.Any())
                m_pass = false;
        }
        private IEnumerable<object> EnforceNoNestedProjects() {
            var projects = LabFolder.Get<TfsProject>(typeof(TfsCsProject), typeof(TfsVcxProject));

            var nestedProjects =
               (from project in projects
                from parent in projects
                where project.Url.IsSubUrl(parent.Url)
                select new { project, parent }).ToArray();

            if (nestedProjects.Any()) {
                yield return new TextHeader("The following projects live in a sub-directory of another project:") {
                    from o in nestedProjects
                    select new TextJoin(" => ") { o.project.Path, o.parent.Path }
                };

                throw new ArgumentException();
            }
        }

        public IEnumerable<object> Execute() {
            if (ToolsVersion != null)
                EnforceToolsVersion();

            if (SolutionVersion != null)
                yield return EnforceSolutionVersion();

            if (BuildEvents)
                yield return EnforceBuildEvents();

            if (NoNestedProjects)
                yield return EnforceNoNestedProjects();

            if (TargetFrameworkVersion != null)
                EnforceTargetFrameworkVersion();

            if (!m_pass && !Fix)
                throw new ArgumentException("Test failed.");

            if (Fix)
                Collection.WriteChanges();
        }

        /// <summary>
        /// Enforce that projects use the same version of msbuild and associated tools. 
        /// </summary>
        [CommandSwitch(1)]
        public double? ToolsVersion { get; set; }

        /// <summary>
        /// Enforce that projects use the same version of Visual Studio. Must be 2010 or 2008. 
        /// </summary>
        [CommandSwitch(2)]
        public int? SolutionVersion { get; set; }

        /// <summary>
        /// Disallow pre/post build events during msbuilds. This prevents introduction of hidden dependencies.
        /// Instead, only allow pre/post build events to execute when projects are built insdie Visual Studio.
        /// Do this by requiring those events be wrapped in IF "$(BuildingInsideVisualStudio)" == "true" (...).
        /// </summary>
        [CommandSwitch(3)]
        public bool BuildEvents { get; set; }

        /// <summary>
        /// Enforce projects do not live in a sub-directory of another project.
        /// </summary>
        [CommandSwitch(4)]
        public bool NoNestedProjects { get; set; }

        [CommandSwitch(5)]
        public double? TargetFrameworkVersion { get; set; }

        /// <summary>Wrap bad postbuild events in IF "$(BuildingInsideVisualStudio)" == "true" (...).</summary>
        [CommandSwitch(5)]
        public bool Fix { get; set; }
    }

    /// <summary>
    /// Enfore a set of configurations across all projects in the lab. Detect and strip all other configurations.
    /// </summary>
    [Command]
    public class Configuration : TfsCommand {

        public Configuration() {
        }

        private void DoDefault(ITfsConfigurable configurable, List<string> verbose) {
            var requiredDefaultConfiguration =
                TfsConfiguration.Get(Flavor.FirstOrDefault(), Platform.FirstOrDefault());

            var defaultConfiguration = configurable.DefaultConfiguration;
            if (defaultConfiguration != null) {
                if (defaultConfiguration != requiredDefaultConfiguration) {
                    verbose.Add("    default: +{0}".Substitute(defaultConfiguration));
                    if (Fix) {
                        if (configurable.Configurations().Contains(requiredDefaultConfiguration)) {
                            configurable.DefaultConfiguration = requiredDefaultConfiguration;
                            verbose.Add("        default fixed.");
                        } else {
                            verbose.Add("        default not fixed because project does not contatin required default configuration.");
                        }
                    }
                }
            }
        }
        private void DoConfigurable(HashSet<TfsConfiguration> requiredConfigurations, ITfsConfigurable configurable, List<string> verbose) {

            if (Default)
                DoDefault(configurable, verbose);

            List<TfsConfiguration> missing = new List<TfsConfiguration>();
            List<TfsConfiguration> extra = new List<TfsConfiguration>();
            foreach (var configuration in configurable.Configurations().Union(requiredConfigurations).OrderBy(x => x.ToString())) {
                var isExtra = !requiredConfigurations.Contains(configuration);
                var isMissing = !configurable.Configurations().Contains(configuration);
                var isExisting = configurable.Configurations().Contains(configuration);

                if ((Missing && isMissing) || (Extra && isExtra) || (Existing && isExisting) || (!Existing && !Missing && !Extra))
                    verbose.Add("    {0}{1}{2}".Substitute(isExtra ? "+" : null, isMissing ? "-" : null, configuration));

                if (isMissing) missing.Add(configuration);
                if (isExtra) extra.Add(configuration);
            }

            if (Fix && extra.Any()) {
                if (configurable is TfsSolution) {
                    if (extra.Count() == configurable.Configurations().Count()) {
                        Console.WriteLine("Cannot remove extra configurations from " + Environment.NewLine +
                            ((TfsPath)configurable).Path + " because there would be no configurations left in the solution.");
                        return;
                    }

                } else {
                    if (missing.Any()) {
                        Console.WriteLine("Cannot remove extra configurations from " + Environment.NewLine +
                            ((TfsPath)configurable).Path + " because the following required configurations are missing: " +
                            missing.StringJoin(", ", o => o.ToString()));
                        return;
                    }
                }

                foreach (var configuration in extra) {
                    configurable.RemoveConfiguration(configuration);
                    Console.WriteLine("    Removing " + configuration);
                }
            }
        }

        public IEnumerable<object> Execute() {
            LabFolder.LoadProjectGraph();

            var requiredConfigurations =
               (from flavor in Flavor
                from platform in Platform
                select TfsConfiguration.Get(flavor, platform))
                .ToHashSet();

            var configurables =
               (from configurable in CurrentFolder.Get<ITfsConfigurable>()
                let item = (TfsItem)configurable
                where IncludeItem(item.Name)
                orderby item.Path
                select configurable).ToList();

            foreach (var configurable in configurables) {

                var verbose = new List<string>();

                DoConfigurable(requiredConfigurations, configurable, verbose);

                if (!verbose.Any())
                    continue;

                var item = (TfsItem)configurable;
                Console.WriteLine(item.Path);

                if (Verbose) {
                    foreach (var c in verbose)
                        Console.WriteLine(c);

                    Console.WriteLine();
                }
            }

            if (Fix)
                Collection.WriteChanges();

            return null;
        }

        private bool IncludeItem(string name) {
            // compose regex for testing solution and project names for exclusion
            var includeRegex = new Regex(Include.Select(o => "^" + o.Replace("*", ".*") + "$").StringJoin("|") ?? ".");

            return includeRegex.IsMatch(name);
        }

        /// <summary>A list of required configurations.</summary>
        [CommandSwitch(1)]
        public TfsFlavor[] Flavor { get; set; }

        /// <summary>A list of required platforms.</summary>
        [CommandSwitch(2)]
        public TfsPlatform[] Platform { get; set; }

        /// <summary>A project name filter. (e.g. *.csproj,*.wdproj)</summary>
        [CommandSwitch(3)]
        public string[] Include { get; set; }

        /// <summary>Filter for projects with missing configurations.</summary>
        [CommandSwitch(4)]
        public bool Missing { get; set; }

        /// <summary>Filter for projects with extra configurations.</summary>
        [CommandSwitch(5)]
        public bool Extra { get; set; }

        /// <summary>Display existing configurations.</summary>
        [CommandSwitch(6)]
        public bool Existing { get; set; }

        /// <summary>
        /// Display bad default configurations. The first flavor and 
        /// platform is the required default configuration.
        /// </summary>
        [CommandSwitch(7)]
        public bool Default { get; set; }

        /// <summary>Display the missing or extra configurations.</summary>
        [CommandSwitch(8)]
        public bool Verbose { get; set; }

        /// <summary>
        /// If /default then set default project configurations to the 
        /// required project configurations. If /extra then strip extra 
        /// project configuration types. 
        /// </summary>
        [CommandSwitch(9)]
        public bool Fix { get; set; }
    }

    /// <summary>
    /// Generate a solution of containing all projects in all sub-directories. 
    /// </summary>
    [Command]
    public class Generate : TfsCommand {

        public Generate(string path)
            : base(path) {
        }

        public void Execute() {

            LabFolder.LoadProjectGraph();

            var solutions = LabFolder.Get<TfsSolution>();

            // create a new solution
            var newSolution = CurrentFolder.NewSolution(Name, Version, UnmanagedPlatform);

            // find all projects in subfolders of the working folder
            var projects =
               (from project in CurrentFolder.Get<TfsProject>()
                where project.Guid != null
                where IncludeItem(project.Name)
                orderby project.Url.AbsoluteUri
                select project).ToArray();

            // create solution folders to mirror actual folders
            var folders = projects.SelectMany(project => project.Folder.ToLinkList(o => o.Parent, o => o != CurrentFolder)).Distinct();
            var subFoldersByFolder = folders.GroupBy(o => o.Parent).ToDictionary(o => o.Key);
            var solutionFoldersByFolder = folders.ToDictionary(o => o, o => newSolution.AddSolutionFolder(o.Name, o.ServerPath));
            foreach (var pair in solutionFoldersByFolder.OrderBy(o => o.Key.Path)) {
                var folder = pair.Key;
                var solutionFolder = pair.Value;

                var subFolders = subFoldersByFolder.GetValueOrDefault(folder);
                if (subFolders != null) {
                    foreach (var subFolder in subFolders)
                        solutionFolder.AddSubFolder(solutionFoldersByFolder[subFolder]);
                }
            }

            // add projects to folders
            foreach (var project in projects)
                solutionFoldersByFolder[project.Folder].AddItem(project);

            // find dependencies for a given project
            var dependencies =
               (from solution in CurrentFolder.Get<TfsSolution>()
                where IncludeItem(solution.Name)
                from o in solution.Dependencies()
                where IncludeItem(o.Key.Name)
                from dependency in o
                where IncludeItem(dependency.Name)
                orderby dependency.GetGuid(solution)
                group dependency by o.Key).ToArray();

            // register dependecies
            foreach (var dependeciesForProject in dependencies) {
                var project = dependeciesForProject.Key as TfsProject;
                if (project == null || project.Guid == null)
                    continue;

                foreach (var dependency in dependeciesForProject.Distinct())
                    newSolution.AddDependency(project, dependency);
            }

            if (Verify) {
                var oldSolution = Collection.Get<TfsSolution>(Name);
                if (oldSolution == null)
                    throw new ArgumentException("No solution file exists at " + Name);
                var oldSolutionFile = oldSolution.ReadToEnd();
                var newSolutionFile = newSolution.ToSolutionFile();
                if (oldSolutionFile != newSolutionFile) {
                    var temp = Path.GetTempFileName();
                    File.WriteAllText(temp, newSolutionFile);
                    throw new ArgumentException("The new solution file would be different than the old one. See " + temp);
                }

            } else {
                Collection.WriteChanges();
            }
        }

        private bool IncludeItem(string name) {
            // compose regex for testing solution and project names for exclusion
            var excludeRegex = new Regex(Exclude.Select(o => "^" + o.Replace("*", ".*") + "$").StringJoin("|") ?? "$^");
            var includeRegex = new Regex(Include.Select(o => "^" + o.Replace("*", ".*") + "$").StringJoin("|") ?? ".");

            return includeRegex.IsMatch(name) && !excludeRegex.IsMatch(name);
        }

        /// <summary>
        /// The name of the solution to generated. Relative paths are resolved from the working directory.
        /// </summary>
        [CommandTarget(1, IsRelativeToCurrentDirectory = true, DefaultValue = "_build.sln")]
        public string Name { get; set; }

        /// <summary>
        /// The version of Visual Studio to target. Must be 2010 or 2008.
        /// </summary>
        [CommandSwitch(1, DefaultValue = 2010)]
        public int Version { get; set; }

        /// <summary>
        /// The platform version of unmanaged projects to target. Must be x64 or x86.
        /// </summary>
        [CommandSwitch(2, DefaultValue = "x64")]
        public string UnmanagedPlatform { get; set; }

        /// <summary>
        /// Patterns of solution and\or project names to exclude. 
        /// For example, "_*.sln" will exclude all solution files starting with underscore.
        /// </summary>
        [CommandSwitch(3)]
        public string[] Exclude { get; set; }

        /// <summary>
        /// Patterns of project names to include. If not specified then all solutions and known
        /// project types are included. For example, "*.sln;*.csproj" will exclude all solutions
        /// and C# projects.
        /// </summary>
        [CommandSwitch(4)]
        public string[] Include { get; set; }

        /// <summary>
        /// Verifies that the generated file matches the existing file.
        /// </summary>
        [CommandSwitch(5)]
        public bool Verify { get; set; }
    }

    /// <summary>
    /// Deletes all files not already in or pending additions to source control.
    /// </summary>
    [Command]
    public class Scorch : TfsCommand {

        public void Execute() {
            var paths = CurrentFolder.Paths().ToList();

            var folders = paths.WhereIs<TfsFolder>().Select(o => o.Path);
            var directories = Directory.GetDirectories(CurrentFolder.Path, "*", SearchOption.AllDirectories).Select(o => o.NormalizeDirectory());
            var extraDirectories = directories.Except(folders, StringComparer.InvariantCultureIgnoreCase);
            extraDirectories = extraDirectories.RootDirectories().ToList();

            var files = Directory.GetFiles(CurrentFolder.Path, "*", SearchOption.AllDirectories).ExceptDirs(extraDirectories);
            var items = paths.WhereIs<TfsItem>().Select(o => o.Path);
            var extraFiles = files.Except(items, StringComparer.InvariantCultureIgnoreCase).ToList();

            foreach (var extraDirectory in extraDirectories) {
                foreach (var extraFile in Directory.GetFiles(extraDirectory, "*", SearchOption.AllDirectories)) {
                    Console.WriteLine(extraFile);
                    if (Preview)
                        continue;

                    DeleteFile(extraFile);
                }
            }

            foreach (var extraFile in extraFiles) {
                Console.WriteLine(extraFile);
                if (Preview)
                    continue;

                DeleteFile(extraFile);
            }

            foreach (var extraDirectory in extraDirectories) {

                foreach (var subDirectory in Directory.GetDirectories(extraDirectory, "*", SearchOption.AllDirectories).OrderByDescending(o => o.Length)) {
                    if (!Preview)
                        Directory.Delete(subDirectory);
                }

                Console.WriteLine(extraDirectory);
                if (Preview)
                    continue;

                Directory.Delete(extraDirectory);
            }
        }

        private void DeleteFile(string path) {
            File.SetAttributes(path, File.GetAttributes(path) & ~FileAttributes.ReadOnly);
            File.Delete(path);
        }

        [CommandSwitch(1)]
        public bool Preview { get; set; }
    }
}
